#include "Headers.h"

const char *HairFileName = "straight.hair";
float *dirs = new float[480000];
cyHairFile myHairModel;
const unsigned short *segments;

const int SAMPLE_COUNT = 32;

GLFrame             viewFrame;
GLFrame             modelMat;
GLFrustum           viewFrustum;
GLMatrixStack		modelViewMatrix;
GLMatrixStack		projectionMatrix;
GLGeometryTransform	transformPipeline;
GLShaderManager		shaderManager;

GLFrame             lightFrame;
GLFrustum           lightFrustum;
GLMatrixStack		lightModelViewMatrix;
GLMatrixStack		lightProjectionMatrix;
GLGeometryTransform	lightTransformPipeline;

bool mouseLeftDown;
bool mouseRightDown;
float mouseX, mouseY;
float cameraAngleX = 0;
float cameraAngleY = 0;
float cameraDistance;
float cameraView;
//float lightAngle = 0;

GLfloat clipToTextureMatrix[] = {0.5, 0, 0, 0,
								0, 0.5, 0, 0,
								0, 0, 0.5, 0,
								0.5, 0.5, 0.5, 1};

GLuint depthFbo, floorFbo;
GLuint depthTexture;
//int sampleCount = 32;

GLuint hairVao, hairBuf, floorVao, floorBuf;
GLBatch floorBatch;
GLTriangleBatch sphereBatch;

GLuint floorShader;
GLint FS_mvp, FS_texMat, FS_lightColor, FS_depthTex, FS_sampleWeight;

GLuint sampleWeightBuf, texBOTexture;
GLfloat sampleWeights[SAMPLE_COUNT];

GLfloat vBlue[] = { 0.0f, 0.0f, 1.0f, 1.0f };
GLfloat vYellow[] = {0.5f, 0.5f, 0.0f, 1.0f};
GLfloat vWhite[] = {1.0f, 1.0f, 1.0f, 1.0f};
GLfloat vGray[] = {0.7f, 0.7f, 0.7f, 1.0f};

const int TEX_HEIGHT = 512;
const int TEX_WIDTH = 512;

void LoadHairModel( const char *filename, cyHairFile &hairfile, float *&dirs )
{
    // Load the hair model
    int result = hairfile.LoadFromFile( filename );

    // Check for errors
    switch( result ) {
        case CY_HAIR_FILE_ERROR_CANT_OPEN_FILE:
            printf("Error: Cannot open hair file!\n");
            return;
        case CY_HAIR_FILE_ERROR_CANT_READ_HEADER:
            printf("Error: Cannot read hair file header!\n");
            return;
        case CY_HAIR_FILE_ERROR_WRONG_SIGNATURE:
            printf("Error: File has wrong signature!\n");
            return;
        case CY_HAIR_FILE_ERROR_READING_SEGMENTS:


            printf("Error: Cannot read hair segments!\n");
            return;
        case CY_HAIR_FILE_ERROR_READING_POINTS:
            printf("Error: Cannot read hair points!\n");
            return;
        case CY_HAIR_FILE_ERROR_READING_COLORS:
            printf("Error: Cannot read hair colors!\n");
            return;
        case CY_HAIR_FILE_ERROR_READING_THICKNESS:
            printf("Error: Cannot read hair thickness!\n");
            return;
        case CY_HAIR_FILE_ERROR_READING_TRANSPARENCY:
            printf("Error: Cannot read hair transparency!\n");
            return;
        default:
            printf("Hair file \"%s\" loaded.\n", filename);
    }

    int hairCount = hairfile.GetHeader().hair_count;
    int pointCount = hairfile.GetHeader().point_count;
    printf("Number of hair strands = %d\n", hairCount );
    printf("Number of hair points = %d\n", pointCount );

    // Compute directions
    if( hairfile.FillDirectionArray( dirs ) == 0 ) {
        printf("Error: Cannot compute hair directions!\n");
    }
    
    glGenVertexArrays(1, &hairVao);
    glBindVertexArray(hairVao);
    
    glGenBuffers(1, &hairBuf);
    glBindBuffer(GL_ARRAY_BUFFER, hairBuf);
    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * pointCount * 3 * 2, NULL, GL_STATIC_DRAW);
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float) * pointCount * 3, hairfile.GetPointsArray());
    glBufferSubData(GL_ARRAY_BUFFER, sizeof(float) * pointCount * 3, sizeof(float) * pointCount * 3, dirs);
    glEnableVertexAttribArray(GLT_ATTRIBUTE_VERTEX);
    glEnableVertexAttribArray(GLT_ATTRIBUTE_NORMAL);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)0);
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)(sizeof(float) * pointCount * 3));
    
    glBindVertexArray(0);
}

void DrawHairModel( const cyHairFile &hairfile, float *dirs )
{
	glBindVertexArray(hairVao);
	
	int pointIndex = 0;
    glBindBuffer(GL_ARRAY_BUFFER, hairBuf);
    int hairCount = hairfile.GetHeader().hair_count;
    const unsigned short *segments = hairfile.GetSegmentsArray();
    if ( segments ) {
        // If segments array exists
        for ( int hairIndex=0; hairIndex < hairCount; hairIndex++ ) {
            glDrawArrays( GL_LINE_STRIP, pointIndex, segments[ hairIndex ]+1 );
            pointIndex += segments[ hairIndex ]+1;
        }
    } else {
        // If segments array does not exist, use default segment count
        int dsegs = hairfile.GetHeader().d_segments;
        for ( int hairIndex=0; hairIndex < hairCount; hairIndex++ ) {
            glDrawArrays( GL_LINE_STRIP, pointIndex, dsegs+1 );
            pointIndex += dsegs+1;
        }
    }
    
    glBindVertexArray(0);
	
}



void SetupRC()
{
	glClearColor(0.05f, 0.05f, 0.05f, 1.0f );

	glEnable(GL_DEPTH_TEST);
	glEnable(GL_MULTISAMPLE);

	shaderManager.InitializeStockShaders();
	
	viewFrame.MoveForward(-150.0f);
	
	viewFrame.MoveUp(0.0f);
	
	lightFrame.MoveForward(-200.0f);
	lightFrame.MoveUp(10.0f);
	
	modelMat.RotateWorld(m3dDegToRad(90), 1, 0, 0);
	modelMat.RotateLocal(m3dDegToRad(180), 0, 0, 1);
	
	//Initialize the light transform pipeline.
	lightFrustum.SetPerspective(80.0f, 1.0f, 50.0f, 500.0f);
	lightProjectionMatrix.LoadMatrix(lightFrustum.GetProjectionMatrix());
	lightTransformPipeline.SetMatrixStacks(lightModelViewMatrix, lightProjectionMatrix);
	

	glGenFramebuffers(1, &depthFbo);
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, depthFbo);

	glActiveTexture(GL_TEXTURE0);
	glGenTextures(1, &depthTexture);
	//glBindTexture(GL_TEXTURE_2D, depthTexture);
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, depthTexture);
	
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	//glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32, TEX_WIDTH, TEX_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
	glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, SAMPLE_COUNT, GL_DEPTH_COMPONENT32, TEX_WIDTH, TEX_HEIGHT, GL_TRUE);
	
	glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D_MULTISAMPLE, depthTexture, 0);
	
	
	//----------Prepare for multisampling, begin---------------
	//int sampleCount = 0;
	
	//glGetIntegerv(GL_SAMPLES, &sampleCount);
	//cout<<"total number of samples per pixel : "<<sampleCount<<endl;
	
	float positions[SAMPLE_COUNT * 2];
	for(int i = 0 ; i < SAMPLE_COUNT; ++i)
		glGetMultisamplefv(GL_SAMPLE_POSITION, i, &positions[i*2]);
		
	float invertedSampleDistances[SAMPLE_COUNT];//manually setup
	float maxDist = 1.0f;

    for(int i=0; i<SAMPLE_COUNT; i++)
    {
        double xDist = positions[i*2  ]-0.5;
        double yDist = positions[i*2+1]-0.5;
        invertedSampleDistances[i] = maxDist - sqrt(xDist*xDist + yDist*yDist);
    }
    
    float totalWeight = 0.0f;
    for(int j=0; j<SAMPLE_COUNT; j++)
		totalWeight += invertedSampleDistances[j];
    
    // Invert to get the factor used for each sample, the sum of all sample weights is always 1.0
	float perSampleFactor = 1.0 / totalWeight;
    for(int j=0; j<SAMPLE_COUNT; j++)
		sampleWeights[j] = invertedSampleDistances[j] * perSampleFactor;
    
    glGenBuffers(1, &sampleWeightBuf);
    glBindBuffer(GL_TEXTURE_BUFFER, sampleWeightBuf);
    glBufferData(GL_TEXTURE_BUFFER, sizeof(float)*SAMPLE_COUNT, sampleWeights, GL_DYNAMIC_DRAW);
    glBindBuffer(GL_TEXTURE_BUFFER, 0);
    
    glActiveTexture(GL_TEXTURE1);
    glGenTextures(1, &texBOTexture);
	glBindTexture(GL_TEXTURE_BUFFER, texBOTexture);
	glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, sampleWeightBuf); 
	//----------Prepare for multisampling, end---------------	
	
	
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
	
	LoadHairModel(HairFileName, myHairModel, dirs);
	
	
	//Draw the floor
	GLfloat vFloor[] = {-1500, -40, -1500, 1500, -40, -1500, 1500, -40, 1500, -1500, -40, 1500};
	floorBatch.Begin(GL_QUADS, 4);
	floorBatch.CopyVertexData3f(vFloor);
	floorBatch.End();
	
	gltMakeSphere(sphereBatch, 2.f, 26, 13);
	
	//floorShader
	floorShader = gltLoadShaderPairWithAttributes("floorShader.vp", "floorShader.fp", 1, GLT_ATTRIBUTE_VERTEX, 
		"pos");
		
	FS_mvp = glGetUniformLocation(floorShader, "modelViewProj");
	FS_texMat = glGetUniformLocation(floorShader, "texMat");
	FS_depthTex = glGetUniformLocation(floorShader, "depthTex");
	FS_lightColor = glGetUniformLocation(floorShader, "lightColor");
	FS_sampleWeight = glGetUniformLocation(floorShader, "sampleWeight");
	
	GLenum errCode;
	const GLubyte *errString;

	if ((errCode = glGetError()) != GL_NO_ERROR) {
		errString = gluErrorString(errCode);
	   fprintf (stderr, "OpenGL Error: %s\n", errString);
	}
	
}


void ShutdownRC(void)
{
	glDeleteVertexArrays(1, &hairVao);
}


void RenderScene(void)
{
	//Rotate the eye or the hair.
	modelMat.RotateWorld(cameraAngleX, 1, 0, 0);
	modelMat.RotateLocal(cameraAngleY, 0, 0, 1);
	
	cameraAngleX = 0;
	cameraAngleY = 0;
	
	//viewFrame.MoveForward(cameraDistance);
	//viewFrame.RotateWorld(cameraView, 0, 1, 0);
	//viewFrame.RotateLocalX(cameraView);
	//cameraDistance = 0;
	//cameraView = 0;
	
	//Render depth texture
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, depthFbo);
	GLint viewPort[4];
	glGetIntegerv(GL_VIEWPORT, viewPort);
	glViewport(0, 0, TEX_WIDTH, TEX_HEIGHT);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST);

	M3DMatrix44f lightCameraMatrix;
	lightFrame.GetCameraMatrix(lightCameraMatrix, false);
	lightModelViewMatrix.PushMatrix(lightCameraMatrix);

	lightModelViewMatrix.MultMatrix(modelMat);

	shaderManager.UseStockShader(GLT_SHADER_FLAT, lightTransformPipeline.GetModelViewProjectionMatrix(), vBlue);
	DrawHairModel(myHairModel, dirs);
	
	lightModelViewMatrix.PopMatrix();

	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
	
	//Render the hair
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST);

	M3DMatrix44f cameraMatrix;
	viewFrame.GetCameraMatrix(cameraMatrix, false);
	modelViewMatrix.PushMatrix(cameraMatrix);

	modelViewMatrix.MultMatrix(modelMat);

	shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vYellow);
	DrawHairModel(myHairModel, dirs);
	
	modelViewMatrix.PopMatrix();
	
	//Render the floor	
	M3DMatrix44f fs_texMat;
	lightModelViewMatrix.PushMatrix(lightCameraMatrix);
	m3dMatrixMultiply44(fs_texMat, clipToTextureMatrix, lightTransformPipeline.GetModelViewProjectionMatrix());
	lightModelViewMatrix.PopMatrix();
	
	modelViewMatrix.PushMatrix(cameraMatrix);
	
	glUseProgram(floorShader);
	//shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlue);
	glUniformMatrix4fv(FS_mvp, 1, GL_FALSE, transformPipeline.GetModelViewProjectionMatrix());
	glUniformMatrix4fv(FS_texMat, 1, GL_FALSE, fs_texMat);
	glUniform4fv(FS_lightColor, 1, vGray);
	glUniform1i(FS_depthTex, 0);
	glUniform1i(FS_sampleWeight, 1);
	
	floorBatch.Draw();
	glUseProgram(0);
	
	modelViewMatrix.PopMatrix();
	
	//Render light bulb
	modelViewMatrix.PushMatrix(cameraMatrix);
	modelViewMatrix.MultMatrix(lightFrame);
	shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vWhite);
	sphereBatch.Draw();
	modelViewMatrix.PopMatrix();
	
	//gltCheckErrors();
	
	
	glutSwapBuffers();

}


void SpecialKeys(int key, int x, int y)
{
    if(key == GLUT_KEY_UP)
        viewFrame.MoveForward(5.0f);

    if(key == GLUT_KEY_DOWN)
        viewFrame.MoveForward(-5.f);

    if(key == GLUT_KEY_LEFT)
        viewFrame.RotateLocalY(0.1);
      
    if(key == GLUT_KEY_RIGHT)
        viewFrame.RotateLocalY(-0.1);
                        
    // Refresh the Window
    glutPostRedisplay();
}

void ChangeSize(int w, int h)
{
	glViewport(0, 0, w, h);

	viewFrustum.SetPerspective(80.0f, float(w)/float(h), 50.0f, 300.0f);

	projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
	transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}

void Mouse(int button, int state, int x, int y)
{
	mouseX = x;
    mouseY = y;

    if(button == GLUT_LEFT_BUTTON)
    {
        if(state == GLUT_DOWN)
        {
            mouseLeftDown = true;
        }
        else if(state == GLUT_UP)
            mouseLeftDown = false;
    }

    else if(button == GLUT_RIGHT_BUTTON)
    {
        if(state == GLUT_DOWN)
        {
            mouseRightDown = true;
        }
        else if(state == GLUT_UP)
            mouseRightDown = false;
    }
    
}

void MouseMotion(int x, int y)
{
	cameraAngleX = 0;
	cameraAngleY = 0;
	cameraDistance = 0;

	if(mouseLeftDown)
    {
        cameraAngleY += (x - mouseX) * 0.1f;
        cameraAngleX += (y - mouseY) * 0.1f;
        mouseX = x;
        mouseY = y;
    }
    if(mouseRightDown)
    {
		//cameraView = (x - mouseX) * 0.05f;
		//cameraViewX += (y - mouseY) * 0.1f;
        //cameraDistance += (y - mouseY) * 0.2f;
        mouseX = x;
        mouseY = y;
    }


    glutPostRedisplay();
}


static void Idle(void)
{
  //TODO: add code here to transform light position
  glutPostRedisplay();
}


static void Keyboard(unsigned char c, int x, int y)
{
  static int animating = 0,
             repeat = 0;

  switch (c) {
  case ' ':
    animating = !animating; /* Toggle */
    if (animating) {
      glutIdleFunc(Idle);
    } else {
      glutIdleFunc(NULL);
    }    
    break;
    
  case 119:
	viewFrame.MoveForward(5.0f);
	break;

  case 115:
	viewFrame.MoveForward(-5.0f);
	break;
	
  case 97:
    viewFrame.RotateLocalY(0.1);
    break;
  
  case 100:
    viewFrame.RotateLocalY(-0.1);
    break;
    
  case 113:
    viewFrame.RotateLocalX(-0.1);
    break;
    
  case 101:
    viewFrame.RotateLocalX(0.1);
    break;
  
  case 27:  /* Esc key */
    exit(0);
    break;
  }
  glutPostRedisplay();
}



int main(int argc, char* argv[])
{
	gltSetWorkingDirectory(argv[0]);
		
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL| GLUT_MULTISAMPLE);
    glutInitWindowSize(TEX_WIDTH,TEX_HEIGHT);
  
    glutCreateWindow("Deep Opacity Maps demo");
 
    glutReshapeFunc(ChangeSize);
    glutDisplayFunc(RenderScene);
	glutKeyboardFunc(Keyboard);
	glutMouseFunc(Mouse);
    glutMotionFunc(MouseMotion);
    glutSpecialFunc(SpecialKeys);

	GLenum err = glewInit();
	if (GLEW_OK != err) {
		fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
		return 1;
		}

    SetupRC();
    glutMainLoop();    
    ShutdownRC();
    return 0;
}